r""" basedata.py

 Provides models and properties for use by all other data classes in
 eCataloger.

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.

 Copyright 2009,2010 Jeremy Nelson
"""
__author__ = "Jeremy Nelson"
__version__ = "1.2"

import datetime,os,logging

from google.appengine.ext import db
from google.appengine.api import users

r"""
  Common custom Properties used in other eCataloger modules.
"""
class BaseDate(object):
    r""" BaseDate provides a flexible date container allowing for variable
    date representations common in biblographic records. For example, if
    an author's birth year is only know, BaseDate can represent just an year.
    BaseDate can also represent a common serials date construction base on
    season and year, such as Winter 2010.

    BaseDate also has allows for specific date encoding parsing such as
    ISO, MARC, and W3C recommendations. BaseDate can also output its
    values in those properties as well. BaseDate also has a number of qualifier
    properties including a "start" BaseDate and an "end" Basedate.
    """
    encoding_dict = {None:0,
                     "w3cdtf":1,
                     "iso8601":2,
                     "marc":3}

    points_dict = {None:0,
                   "start":1,
                   "end":2}

    qualifier_dict = {None:0,
                      "approximate":1,
                      "inferred":2,
                      "questionable":3}

    # Stores additional time period in the months
    # column
    season_dict = {"fall":13,
                   "winter":14,
                   "spring":15,
                   "summer":16}

    def __init__(self,
                 year=0,
                 month=0,
                 day=0,
                 encoding=None,
                 point=None,
                 isKey=False,
                 qualifier=None):
        """ BaseDate constructor.

        Keyword arguments:
        year -- Year integer, default is 0
        month -- Month integer, should be between 1-16 with season 13-16, default is 0
        day -- Day integer, should be between 1-31, error checking for invalid month/day combinations, default is 0
        encoding -- Default is None, error checking for string in BaseDate.encoding_dict
        point -- Default is None, valid values are "start" and "end" in BaseDate.points_dict
        isKey -- Boolean, default is False, used for MODS compatibility
        qualifier -- From FRBRoo spec, sets three value confidence in BaseDate instance. default is None
        """
        self.year = year
        self.month = month
        self.day = day
        if BaseDate.encoding_dict.has_key(encoding):
            self.encoding = BaseDate.encoding_dict[encoding]
        else:
            raise db.BadValueError('Encoding %s must be one of these '
                                   'values %s' %
                                   (encoding,BaseDate.encoding_dict.keys()))
        if BaseDate.points_dict.has_key(point):
            self.point = BaseDate.points_dict[point]
        else:
            raise db.BadValueError('Point %s must be one of these '
                                   'values %s' %
                                   (point,BaseDate.points_dict.keys()))
        if BaseDate.qualifier_dict.has_key(qualifier):
            self.qualifier = BaseDate.qualifier_dict[qualifier]
        else:
            raise db.BadValueError('Qualifier %s must be one of these '
                                   'values %s' %
                                   (qualifier,BaseDate.qualifier_dict.keys()))
        self.isKeyDate = isKey

    def parseDate(self,
                  raw_date,
                  encoding):
        """ Returns BaseDate from raw_date using encoding

        Keyword arguments:
        raw_date -- Required string with encoding's date format, ex: 19990101 for iso8601
        encoding -- Required string, must be in the BaseDate.encoding_dict
        """
        if len(raw_date) <= 4 and int(raw_date):
            self.year=int(raw_date)
            self.encoding = self.encoding_dict[encoding]
            return self
        else:
            if encoding == "iso8601":
                if len(raw_date) == 8: # Format YYYYMMDD
                    self.year=int(raw_date[:4])
                    self.month=int(raw_date[4:6])
                    self.day=int(raw_date[6:8])
                    self.encoding = BaseDate.encoding_dict[encoding]
                    return self
                
                if len(raw_date) == 6: # Format YYYYMM
                    self.year=int(raw_date[:4])
                    self.month=int(raw_date[4:6])
                    self.encoding = BaseDate.encoding_dict[encoding]
                    return self
                else:
                    raise ValueError("Unknown value=%s for iso8601 encoding" %\
                                       raw_date)
                    
            elif encoding == "w3cdtf":
                delimiter = "-"
            elif encoding == "marc":
                delimiter = '/'
            else:
                raise ValueError("Unknown encoding %s for value=%s" %\
                                 (encoding,raw_date))
            date_list = raw_date.split(delimiter)
            if len(date_list) == 2: # Only year and month are present
                self.year=int(date_list[0])
                self.month=int(date_list[1])
                self.encoding = BaseDate.encoding_dict[encoding]
                return self
            
            elif len(date_list) == 3:
                self.year = year=int(date_list[0])
                self.month = year=int(date_list[1])
                self.day=int(date_list[2])
                self.encoding = BaseDate.encoding_dict[encoding]
                return self
            else:
                raise ValueError('''Unknown list=%s for %s encoding,
                                    raw value=%s'''%\
                                 (date_list,encoding,raw_dae))
            
            

    def has_day(self):
        """ Returns integer if day, otherwise False."""
        return self.day > 0

    def has_month(self):
        """ Returns integer between month between 1-12, otherwise False."""
        return self.month > 0 and self.month < 13

    def has_year(self):
        """ Returns integer if year, otherwise False."""
        return self.year > 0

    def has_encoding(self):
        """ Returns integer if encoding, otherwise False."""
        return self.encoding > 0

    def has_point(self):
        """ Returns integer if point, otherwise False."""
        return self.point > 0

    def has_qualifier(self):
        """ Returns integer if qualifier, otherwise False."""
        return self.qualifier > 0

    def has_season(self):
        """ Returns integer if month is > 12, otherwise False."""
        return self.month > 12

    def is_keyDate(self):
        """ Returns boolean of keyDate value."""
        return self.isKeyDate

    def reverse_test(self,tst_dict,value):
        """ Convenience method for converting stored BaseDate values
       (stored as integers in datastore) to string representation.

       Keyword arguments:
       tst_dict -- Required, dictionary of strings with corresponding int values
       value -- Required, stored BaseDate integer property
       """
        r_dict = {}
        for i,t in tst_dict.iteritems():
            r_dict[t] = i
        if r_dict.has_key(value):
            return r_dict[value]
        else:
            return None

    def get_encoding(self):
        """ Returns string of BaseDate instance's stored encoding value or None.
        """
        if self.has_encoding():
            return self.reverse_test(BaseDate.encoding_dict,
                                     self.encoding)
        else:
            return None
            

    def get_qualifier(self):
        """ Returns string of BaseDate instance's stored qualifier value or None.
        """        
        if self.has_qualifier():
            return self.reverse_test(BaseDate.qualifier_dict,
                                     self.qualifier)
        else:
            return None

    def get_point(self):
        """ Returns string of BaseDate instance's stored point value or None.
        """
        if self.has_point():
            return self.reverse_test(BaseDate.points_dict,
                                     self.point)
        else:
            return None

    def get_season(self):
        """ Returns season string of BaseDate instance's stored month value or None.
        """
        if self.has_season():
            return self.reverse_test(BaseDate.season_dict,
                                     self.month)
        else:
            return None

    def get_pretty_month(self):
        """ Returns month string of BaseDate instance's stored month value or None.
        """
        if self.has_month():
            # Create tmp date object
            tmp_date = datetime.date(1900,self.month,1)
            return tmp_date.strftime("%B")
        elif self.has_season():
            return self.get_season().title()
        else:
            return None

    def __str__(self):
        """ Returns string representation of BaseDate values based on encoding and values."""
        if self.has_day():
            if self.encoding == 1:
                format = "%04d-%02d-%02d" % (self.year,
                                             self.month,
                                             self.day)
            elif self.encoding == 2:
                format = "%04d%02d%02d" % (self.year,
                                           self.month,
                                           self.day)
            else:
                format = "%02d/%02d/%04d" % (self.month,
                                             self.day,
                                             self.year)
            return format
        if self.has_month():
            if self.encoding == 1:
                format = "%04d-%02d" % (self.year,self.month)
            elif self.encoding == 2:
                format = "%04d%02d" % (self.year,self.month)
            else:
                format = "%02d/%04d" % (self.month,self.year)
            return format
        if self.has_season():
            format = "%s %04d" % (self.get_season().title(),self.year)
        if self.has_year():
            return "%04d" % self.year
    
    def __not__(self):
        """ Return negation of values, used for GAE storage."""
        return (not (self.has_year() or
                self.has_month() or
                self.has_day()))


class BaseDateProperty(db.Property):
    r""" BaseDateProperty class extends GAE db.Property and stores/retrieves
     BaseDate entities.
     """

    data_type = BaseDate

    def get_value_for_datastore(self,model_instance):
        r""" Returns list of ints and boolean representation for saving in
        GAE datastore or None.

        Keyword arguments:
        model_instance -- BaseDate instance
        """
        
        date = super(BaseDateProperty,
                     self).get_value_for_datastore(model_instance)
        if date:
        # Stores actual date as int
            date_int = (date.year * 10000) + (date.month * 100) + date.day
            date_info = (date.encoding * 10000) + (date.qualifier * 100) + date.point
            return [date_int,date_info,date.isKeyDate]
        else:
            return None

    def get_value_dict(self,tst_dict,value):
        r""" Returns reversed key:value of input dictionary. This method
        supports other class methods.

        Keyword arguments:
        tst_dict -- Input dictionary for reversal
        """
        r_dict = {}
        for i,t in tst_dict.iteritems():
            r_dict[t] = i
        if r_dict.has_key(value):
            return r_dict[value]
        else:
            return None

    def make_value_from_datastore(self,value):
        r""" Returns BaseDate instance from retrieved list of ints and boolean
        from GAE datastore.

        Keyword arguments:
        value -- list with first item a date int, second item date info int, and third list item a boolean
        """
        if value is None:
            return None
        date_int = value[0]
        date_info = value[1]
        isKeyDate = value[2]
        year = date_int / 10000
        month = (date_int / 100) % 100
        day = date_int % 100
        encoding = date_info / 10000
        qualifier = (date_info / 100) % 100
        point = date_info % 100
        return BaseDate(year=year,
                        month=month,
                        day=day,
                        encoding=self.get_value_dict(BaseDate.encoding_dict,encoding),
                        point=self.get_value_dict(BaseDate.points_dict,point),
                        isKey=isKeyDate,
                        qualifier=self.get_value_dict(BaseDate.qualifier_dict,qualifier))

    
    def validate(self,value):
        r""" Returns validation of db.Property class on value. Throws
        BadValueError if value is none or not a BaseDate instance.

        Keyword arguments:
        value -- BaseDate instance
        """
        if value is not None and not isinstance(value,BaseDate):
            raise BadValueError('Property %s must be convertable '
                                'to a BaseDate instance (%s)',
                                (self.name,value))
        return super(BaseDateProperty,self).validate(value)

    def empty(self,value):
        r""" Returns False for empty Value, true if value is None. Method
        is a helper method for GAE operations.

        Keyword arguments:
        value -- BaseDate value
        """
        return not value


class DateRange(object):
    r""" Used to represent a starting date and an optional ending date.
     Used in FRBR and MODS entities in eCataloger.
     """
    def __init__(self,
                 start_date=None,
                 end_date=None):
        r""" Initization for DateRange.

        Keyword arguments:
        start_date -- BaseDate instance, required.
        end_date -- BaseDate instance, optional. If end_date exists, must be > start_date
        """
        if start_date is None or not isinstance(start_date,BaseDate):
            raise db.BadValueError('DateRange must have a start date '
                                   'and be a BaseDate instance')
        if end_date is not None and not isinstance(end_date,BaseDate):
            raise db.BadValueError('DateRange end date must be a BaseDate instance')
        # Tests that start_date year is less than end_date year
        if end_date:
            if start_date.year > end_date.year:
                raise db.BadValueError('Start year %s must be less than end year %s',
                                       (start_date.year,end_date.year))
            end_date.point = BaseDate.points_dict['end']
            self.end = end_date
        else:
            self.end = None
        start_date.point = BaseDate.points_dict['start']
        self.start = start_date

    def __str__(self):
        r""" Returns string representation of DateRange values based on encoding and values.
        """
        output_str = ''
        output_str += "%s - " % str(self.start)
        if self.end:
            output_str += str(self.end)
        return output_str

    def __not__(self):
        """ Return negation of instance properties, used for GAE storage."""
        return (not (self.start or
                     self.end))

class DateRangeProperty(db.Property):
    r""" Datastore property for DateRange, stores start and end dates as
        a tuple in GAE datastore.
    """
    data_type = DateRange

    def get_value_for_datastore(self,model_instance):
        r""" Returns list of BaseDate raw lists for ints and boolean
        representations for saving in GAE datastore or None.

        Keyword arguments:
        model_instance -- DateRange instance
        """
        date_range_prop = super(DateRangeProperty,
                                self).get_value_for_datastore(model_instance)
        if date_range_prop:
            if date_range_prop.end:
                return [date_range_prop.start,date_range_prop.end]
            else:
                return [date_range_prop.start,]
        else:
            return None

    def make_value_from_datastore(self,value):
        r""" Returns DateRange instance from retrieved list of ints and boolean
        from GAE datastore.

        Keyword arguments:
        value -- List of one or two BaseDate instances
        """
        start_date = value[0]
        end_date = value[1]
        if end_date:
            date_range_obj = DateRange(start_date=start_date,
                                       end_date=end_date)
        else:
            date_range_obj = DateRange(start_date=start_date)
        return date_range_obj

    def validate(self,value):
        r""" Returns validation of db.Property class on value. Throws
        BadValueError if value is none or not a DateRange instance.

        Keyword arguments:
        value -- DateRange instance
        """        
        if value is not None and not isinstance(value,
                                                DateRange):
            raise db.BadValueError('Property %s must be convertable '
                                   'to a DateRange instance',
                                   (self.name,value))
        return super(DateRangeProperty,
                     self).validate(value)

    def empty(self,value):
        r""" Returns False for empty Value, true if value is None. Method
        is a helper method for GAE operations.

        Keyword arguments:
        value -- DateRange value
        """        
        return not value
        

r"""
   Common Models used through-out eCataloger.
"""

class BaseModel(db.Model):
    r""" BaseModel for all eCataloger data models. 
    Extends GAE db.Model and includes commong properties for most eCataloger
    data classes.
    """
    created_on = db.DateTimeProperty(auto_now_add=True)
    created_by = db.UserProperty()
    display_form = db.StringProperty()
    language = db.StringProperty()
    last_modified = db.DateTimeProperty(auto_now=True)

    def get_collection(self,**args):
        r""" Returns a list of retrieved datastore entities.

        Keyword arguments:
        args['property_name'] -- Name of db.ListProperty 
        """
        property_name = args['property_name']
        entities = getattr(self,property_name)
        if entities:
            output = list()
            for entity_key in entities:
                output.append(db.get(entity_key))
            return output
        else:
            return None

    def recast(self,parent):
        r""" Takes parent instance and copies values to current
        instance.

        Keyword arguments:
        parent -- parent eCataloger instance of current classes' instance
        """
        self.__dict__.update(parent.__dict__)
        return self
        

class DateRangeModel(BaseModel):
    """ Temporary solution to DateRange property problem. """
    start_date = BaseDateProperty()
    end_date = BaseDateProperty()

r"""
 Validation functions                                                         
"""
def compareModels(model_one,model_two):
    r""" Compares all values between model one and model two, returns percentage
        of properties that matches between the models as a value between 0 and
        1.
    """
    properties_same,max_properties_count = 0.0,0.0
    def compareDict(obj1_dict,obj2_dict):
        same_count = 0.0
        for key,value in obj1_dict.iteritems():
            if obj2_dict.has_key(key):
                if value == obj2_dict[key]:
                    same_count += 1
        return same_count
    if len(model_one.__dict__.keys()) >= len(model_two.__dict__.keys()):
        max_properties_count = len(model_one.__dict__.keys())
        properties_same = compareDict(model_one.__dict__,model_two.__dict__)
    else:
        max_properties_count = len(model_two.__dict__.keys())
        properties_same = compareDict(model_two.__dict__,model_one.__dict__)
    return properties_same / max_properties_count
